package ru.testtask.util;

import ru.testtask.util.Config;
import ru.testtask.model.XmlElement;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class XmlParser {

    // normal states
    private static final int START                  = 0;
    private static final int OPEN_OR_CLOSE_EL       = 1;
    private static final int OPEN_EL_NAME           = 2;
    private static final int OPEN_EL_NAME_READY     = 3;
    private static final int ATR_NAME               = 4;
    private static final int ATR_NAME_READY         = 5;
    private static final int ATR_VAL_WAITING        = 6;
    private static final int ATR_VAL_START          = 7;
    private static final int ATR_VAL                = 8;
    private static final int ATR_VAL_FINISH         = 9;
    private static final int OPEN_EL_FINISHING      = 10;
    private static final int OPEN_EL_FINISHED       = 11;
    private static final int CLOSE_EL               = 12;
    private static final int CLOSE_EL_NAME          = 13;
    private static final int CLOSE_EL_NAME_READY    = 14;
    private static final int CLOSE_EL_FINISHED      = 15;

    // error states
    private static final int ERROR                  = -1;
    private static final int ERROR1                 = -2;

    /**
     * State matrix.
     * Each row of this matrix represents state({@link #START}, {@link #OPEN_OR_CLOSE_EL}, etc)
     * Each column represents some type of symbol {@link #getSymTypeIndx(char)}.
     * According to new symbol and current state we can determine new state using this matrix
     */
    private static final int [][] STATE_MATRIX = {
/*                        space OR \n OR \r |  letter      |  digit       |     <             |    >      |    /      |     "        |      =        |   all other */
/*START               */{START,	              ERROR,	     ERROR,	        OPEN_OR_CLOSE_EL,	ERROR,	    ERROR,	    ERROR,	        ERROR,	         ERROR},
/*OPEN_OR_CLOSE_EL    */{ERROR,	              OPEN_EL_NAME,	 ERROR,	        ERROR,	            ERROR,	    CLOSE_EL,	ERROR,	        ERROR,	         ERROR},
/*OPEN_EL_NAME        */{OPEN_EL_NAME_READY,  OPEN_EL_NAME,	 OPEN_EL_NAME,  ERROR,	            ERROR1,	    ERROR,	    ERROR,	        ERROR,	         ERROR},
/*OPEN_EL_NAME_READY  */{OPEN_EL_NAME_READY,  ATR_NAME,	     ERROR,	        ERROR,	            ERROR1,	    ERROR,	    ERROR,	        ERROR,	         ERROR},
/*ATR_NAME            */{ATR_NAME_READY,	  ATR_NAME,	     ATR_NAME,	    ERROR,	            ERROR,	    ERROR,	    ERROR,	        ATR_VAL_WAITING, ERROR},
/*ATR_NAME_READY      */{ATR_NAME_READY,	  ERROR,	     ERROR,	        ERROR,	            ERROR,	    ERROR,	    ERROR,	        ATR_VAL_WAITING, ERROR},
/*ATR_VAL_WAITING     */{ATR_VAL_WAITING,	  ERROR,	     ERROR,	        ERROR,	            ERROR,	    ERROR,	    ATR_VAL_START,	ERROR,	         ERROR},
/*ATR_VAL_START       */{ATR_VAL,             ATR_VAL,       ATR_VAL,       ATR_VAL,            ATR_VAL,    ATR_VAL,	ATR_VAL_FINISH, ATR_VAL,         ATR_VAL},
/*ATR_VAL             */{ATR_VAL,             ATR_VAL,       ATR_VAL,       ATR_VAL,            ATR_VAL,    ATR_VAL,    ATR_VAL_FINISH, ATR_VAL,         ATR_VAL},
/*ATR_VAL_FINISH      */{OPEN_EL_FINISHING,	  ERROR,	     ERROR,	        ERROR,	     OPEN_EL_FINISHED,	ERROR,	    ERROR,	        ERROR,	         ERROR},
/*OPEN_EL_FINISHING   */{OPEN_EL_FINISHING,	  ERROR,	     ERROR,	        ERROR,	     OPEN_EL_FINISHED,  ERROR,	    ERROR,	        ERROR,	         ERROR},
/*OPEN_EL_FINISHED    */{START,               ERROR,         ERROR,         OPEN_OR_CLOSE_EL,   ERROR,	    ERROR,	    ERROR,	        ERROR,	         ERROR},
/*CLOSE_EL            */{ERROR,               CLOSE_EL_NAME, ERROR,         ERROR,              ERROR,	    ERROR,	    ERROR,          ERROR,           ERROR},
/*CLOSE_EL_NAME       */{CLOSE_EL_NAME_READY, CLOSE_EL_NAME, CLOSE_EL_NAME, ERROR,      CLOSE_EL_FINISHED,  ERROR,	    ERROR,	        ERROR,	         ERROR},
/*CLOSE_EL_NAME_READY */{CLOSE_EL_NAME_READY, ERROR,	     ERROR,	        ERROR,	    CLOSE_EL_FINISHED,  ERROR,	    ERROR,	        ERROR,	         ERROR},
/*CLOSE_EL_FINISHED   */{START,               ERROR,         ERROR,         OPEN_OR_CLOSE_EL,   ERROR,	    ERROR,	    ERROR,	        ERROR,	         ERROR},
    };


    public static List<XmlElement> parseXmlString(String xml) throws XmlParseException {
        List<XmlElement> result = new LinkedList<XmlElement>();

        Queue<XmlElement> stack = Collections.asLifoQueue(new LinkedList<XmlElement>());

        boolean checkEscapeInAttrVal = false;
        int currentState = START;
        String tokenValue;
        StringBuilder strValue = new StringBuilder();
        for (int i = 0; i < xml.length(); i++) {
            char sym = xml.charAt(i);
            int symTypeIndx = getSymTypeIndx(sym);

            int newState = STATE_MATRIX[currentState][symTypeIndx];

            switch (newState) {
                case START:
                case OPEN_OR_CLOSE_EL:
                case ATR_NAME_READY:
                case ATR_VAL_START:
                case OPEN_EL_FINISHING:
                case OPEN_EL_FINISHED:
                case CLOSE_EL:
                case CLOSE_EL_NAME_READY:
                    break;

                case OPEN_EL_NAME:
                case ATR_NAME:
                case CLOSE_EL_NAME:
                    strValue.append(sym);
                    break;
                case ATR_VAL:
                    if (sym == '&') {
                        checkEscapeInAttrVal = true;
                    }
                    strValue.append(sym);
                    break;

                case OPEN_EL_NAME_READY:
                    tokenValue = strValue.toString();
                    if (tokenValue.length() != 0) {
                        if (!Config.getInstance().isElementNameAllowed(tokenValue)) {
                            throw new XmlParseException("Parsing error. Bad elements name: " + tokenValue);
                        }

                        XmlElement xmlElement = new XmlElement(tokenValue);

                        XmlElement parentEl = stack.peek();
                        if (parentEl != null) {
                            parentEl.addChild(xmlElement);
                        } else {
                            result.add(xmlElement);
                        }
                        
                        stack.add(xmlElement);

                        //------------------------
                        strValue = new StringBuilder();
                    }
                    break;
                case ATR_VAL_WAITING:
                    tokenValue = strValue.toString();
                    if (tokenValue.length() != 0) {
                        if (!Config.getInstance().isAttributeNameAllowed(tokenValue)) {
                            throw new XmlParseException("Parsing error. Bad attribute name: " + tokenValue);
                        }

                        XmlElement el = stack.peek();
                        if (el == null) {
                            throw new XmlParseException("Parsing error. No element for attribute found");
                        }
                        el.getAttribute().setName(tokenValue);

                        //------------------------
                        strValue = new StringBuilder();
                    }
                    break;
                case ATR_VAL_FINISH:
                    tokenValue = strValue.toString();
                    XmlElement el = stack.peek();
                    if (el == null) {
                        throw new XmlParseException("Parsing error. No element for attribute found");
                    }

                    if (checkEscapeInAttrVal && !checkEscape(tokenValue)) {
                        throw new XmlParseException("Parsing error. Bad attribute value");
                    }
                    checkEscapeInAttrVal = false;
                    el.getAttribute().setValue(tokenValue);

                    strValue = new StringBuilder();
                    break;
                case CLOSE_EL_FINISHED:
                    tokenValue = strValue.toString();

                    XmlElement elReady = stack.poll();
                    if (elReady == null || !tokenValue.equals(elReady.getName())) {
                        throw new XmlParseException("Parsing error. Open tag for closed tag[" + tokenValue + "] NOT found");
                    }
                    strValue = new StringBuilder();
                    break;

                case ERROR:
                    throw new XmlParseException("Parsing error.");
                    //break;
                case ERROR1:
                    throw new XmlParseException("Parsing error. element might have exactly one attribute");
                    //break;

            }

            currentState = newState;
        }

        if (currentState != START && currentState != CLOSE_EL_FINISHED) {
            throw new XmlParseException("Parsing error.");
        }
        if (stack.peek() != null) {
            throw new XmlParseException("Parsing error.");
        }
        
        return result;

    }

    private static boolean checkEscape(String tokenValue) {
        int state = 3;
        StringBuilder escStr = new StringBuilder();
        for (int i=0; i<tokenValue.length(); i++) {
            char symbol = tokenValue.charAt(i);
            switch (state) {
                case 0: {
                    if (symbol == '#') {
                        state = 1;
                    } else if (Character.isLetter(symbol)) {
                        state = 2;
                        escStr.append(symbol);
                    } else {
                        return false;
                    }
                } break;
                case 1: {
                    if (Character.isDigit(symbol)) {
                        escStr.append(symbol);
                    } else if (symbol == ';') {
                        String esc = escStr.toString();
                        if (esc.length() <=0 || esc.length() > 5) {
                            return false;
                        }
                        state = 3;
                    } else {
                        return false;
                    }
                } break;
                case 2: {
                    if (Character.isLetter(symbol)) {
                        escStr.append(symbol);
                    } else if (symbol == ';') {
                        if (!"quot".equals(escStr.toString())) {
                            return false;
                        }
                        state = 3;
                    } else {
                        return false;
                    }
                } break;
                case 3: {
                    if (symbol == '&') {
                        escStr = new StringBuilder();
                        state = 0;
                    }
                } break;
            }
        }
        return state == 3;
    }

    private static int getSymTypeIndx(char sym) {

        switch (sym) {
            case ' ':
            case '\t':
            case '\n':
            case '\r':
                return 0;
            case '<':
                return 3;
            case '>':
                return 4;
            case '/':
                return 5;
            case '"':
                return 6;
            case '=':
                return 7;
            default: {
                // check if it is digit:
                if (Character.isDigit(sym)) {
                    return 2;
                }
                // check if it is letter:
                if (Character.isLetter(sym)) {
                    return 1;
                }
                return 8;
            }
        }
    }

}
